Micron Document
baubs git

Node / mirrors / respira / commits / 4992c33

Commit 4992c33bf1da1a18c848343b8980b5597f4abbc9


Parents : 8ad8d7c
Author : Jan-Henrik Bruhn <jan-henrik.bruhn@offis.de>
Date : 2025-12-21T13:20:12+01:00

feature: Migrate ProgressMonitor color blocks to shadcn ScrollArea

Replace custom scrollable div implementation with shadcn ScrollArea component
for the color blocks list in ProgressMonitor. This improves code maintainability
and provides consistent styling across the application.

Changes:
- Install @radix-ui/react-scroll-area via shadcn CLI
- Add scroll-area.tsx component with proper @/ path alias
- Replace custom scrollable div with ScrollArea wrapper
- Remove manual scroll handling:
- Removed showGradient state and useState import
- Removed colorBlocksScrollRef ref
- Removed handleColorBlocksScroll function
- Removed resize listener useEffect
- Removed gradient overlay div
- Fix ScrollArea height constraint with lg:h-0 for proper flexbox scrolling
- Simplify component structure with 50+ fewer lines of scroll handling code

The ScrollArea component provides better accessibility and consistent scrollbar
styling while eliminating the need for manual scroll position tracking.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

Changes

4 files changed, 115 insertions(+), 39 deletions(-)


Diff

diff --git a/package-lock.json b/package-lock.json
index ad6c50d..da4d642 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -14,6 +14,7 @@
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-progress": "^1.1.8",
+ "@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-tooltip": "^1.2.8",
@@ -3614,6 +3615,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@radix-ui/number": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
+ "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==",
+ "license": "MIT"
+ },
"node_modules/@radix-ui/primitive": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
@@ -3773,6 +3780,21 @@
}
}
},
+ "node_modules/@radix-ui/react-direction": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
+ "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-dismissable-layer": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz",
@@ -4096,6 +4118,37 @@
}
}
},
+ "node_modules/@radix-ui/react-scroll-area": {
+ "version": "1.2.10",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz",
+ "integrity": "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/number": "1.1.1",
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-separator": {
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.8.tgz",

diff --git a/package.json b/package.json
index 7754ba0..c983a6b 100644
--- a/package.json
+++ b/package.json
@@ -27,6 +27,7 @@
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-progress": "^1.1.8",
+ "@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-tooltip": "^1.2.8",

diff --git a/src/components/ProgressMonitor.tsx b/src/components/ProgressMonitor.tsx
index 793afe6..87128a0 100644
--- a/src/components/ProgressMonitor.tsx
+++ b/src/components/ProgressMonitor.tsx
@@ -1,4 +1,4 @@
-import { useRef, useEffect, useState, useMemo } from "react";
+import { useRef, useEffect, useMemo } from "react";
import { useShallow } from "zustand/react/shallow";
import { useMachineStore } from "../stores/useMachineStore";
import { usePatternStore } from "../stores/usePatternStore";
@@ -26,6 +26,7 @@ import {
} from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Progress } from "@/components/ui/progress";
+import { ScrollArea } from "@/components/ui/scroll-area";
export function ProgressMonitor() {
// Machine store
@@ -52,8 +53,6 @@ export function ProgressMonitor() {
// Pattern store
const pesData = usePatternStore((state) => state.pesData);
const currentBlockRef = useRef<HTMLDivElement>(null);
- const colorBlocksScrollRef = useRef<HTMLDivElement>(null);
- const [showGradient, setShowGradient] = useState(true);
// State indicators
const isMaskTraceComplete =
@@ -135,31 +134,6 @@ export function ProgressMonitor() {
}
}, [currentBlockIndex]);
- // Handle scroll to detect if at bottom
- const handleColorBlocksScroll = () => {
- if (colorBlocksScrollRef.current) {
- const { scrollTop, scrollHeight, clientHeight } =
- colorBlocksScrollRef.current;
- const isAtBottom = scrollTop + clientHeight >= scrollHeight - 5; // 5px threshold
- setShowGradient(!isAtBottom);
- }
- };
-
- // Check initial scroll state and update on resize
- useEffect(() => {
- const checkScrollable = () => {
- if (colorBlocksScrollRef.current) {
- const { scrollHeight, clientHeight } = colorBlocksScrollRef.current;
- const isScrollable = scrollHeight > clientHeight;
- setShowGradient(isScrollable);
- }
- };
-
- checkScrollable();
- window.addEventListener("resize", checkScrollable);
- return () => window.removeEventListener("resize", checkScrollable);
- }, [colorBlocks]);
-
return (
<Card className="p-0 gap-0 lg:h-full border-l-4 border-accent-600 dark:border-accent-500 flex flex-col lg:overflow-hidden">
<CardHeader className="p-4 pb-3">
@@ -242,12 +216,8 @@ export function ProgressMonitor() {
<h4 className="text-xs font-semibold mb-2 text-gray-700 dark:text-gray-300 flex-shrink-0">
Color Blocks
</h4>
- <div className="relative lg:flex-1 lg:min-h-0">
- <div
- ref={colorBlocksScrollRef}
- onScroll={handleColorBlocksScroll}
- className="lg:absolute lg:inset-0 flex flex-col gap-2 lg:overflow-y-auto scroll-smooth pr-1 [&::-webkit-scrollbar]:w-1 [&::-webkit-scrollbar-track]:bg-gray-100 dark:[&::-webkit-scrollbar-track]:bg-gray-700 [&::-webkit-scrollbar-thumb]:bg-primary-600 dark:[&::-webkit-scrollbar-thumb]:bg-primary-500 [&::-webkit-scrollbar-thumb]:rounded-full"
- >
+ <ScrollArea className="lg:flex-1 lg:h-0">
+ <div className="flex flex-col gap-2 pr-4">
{colorBlocks.map((block, index) => {
const isCompleted = currentStitch >= block.endStitch;
const isCurrent = index === currentBlockIndex;
@@ -362,11 +332,7 @@ export function ProgressMonitor() {
);
})}
</div>
- {/* Gradient overlay to indicate more content below - only on desktop and when not at bottom */}
- {showGradient && (
- <div className="hidden lg:block absolute bottom-0 left-0 right-0 h-8 bg-gradient-to-t from-white dark:from-gray-800 to-transparent pointer-events-none" />
- )}
- </div>
+ </ScrollArea>
</div>
)}

diff --git a/src/components/ui/scroll-area.tsx b/src/components/ui/scroll-area.tsx
new file mode 100644
index 0000000..51ecedc
--- /dev/null
+++ b/src/components/ui/scroll-area.tsx
@@ -0,0 +1,56 @@
+import * as React from "react";
+import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
+
+import { cn } from "@/lib/utils";
+
+function ScrollArea({
+ className,
+ children,
+ ...props
+}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
+ return (
+ <ScrollAreaPrimitive.Root
+ data-slot="scroll-area"
+ className={cn("relative", className)}
+ {...props}
+ >
+ <ScrollAreaPrimitive.Viewport
+ data-slot="scroll-area-viewport"
+ className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
+ >
+ {children}
+ </ScrollAreaPrimitive.Viewport>
+ <ScrollBar />
+ <ScrollAreaPrimitive.Corner />
+ </ScrollAreaPrimitive.Root>
+ );
+}
+
+function ScrollBar({
+ className,
+ orientation = "vertical",
+ ...props
+}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
+ return (
+ <ScrollAreaPrimitive.ScrollAreaScrollbar
+ data-slot="scroll-area-scrollbar"
+ orientation={orientation}
+ className={cn(
+ "flex touch-none p-px transition-colors select-none",
+ orientation === "vertical" &&
+ "h-full w-2.5 border-l border-l-transparent",
+ orientation === "horizontal" &&
+ "h-2.5 flex-col border-t border-t-transparent",
+ className,
+ )}
+ {...props}
+ >
+ <ScrollAreaPrimitive.ScrollAreaThumb
+ data-slot="scroll-area-thumb"
+ className="bg-border relative flex-1 rounded-full"
+ />
+ </ScrollAreaPrimitive.ScrollAreaScrollbar>
+ );
+}
+
+export { ScrollArea, ScrollBar };

Served by rngit 1.3.3 - Generated in 0.07s